/*
 * AGENT SHERPA
 *  
 * Assignement 2 of the Agent Oriented Software Engineering Course
 * M.Sc. in Advanced Software Engineering (Part-time)
 * University College Dublin www.ucd.ie
 *
 * Copyright (c) 2007 Chris Walsh, Aidan Morrisy and Iain Hull.
 * All rights reserved.
 */

package org.agentsherpa.model;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Observable;
import java.util.Set;

/**
 * Stores the Map with the Drone and IED coordinates
 * 
 * @author iain
 */
public class SimulatorModel extends Observable {
    private static final int ROAD_SENSE_DISTANCE = 5;
    private static final int DRONE_SENSE_DISTANCE = 5;
    private static final int IED_SENSE_DISTANCE = 1;
    private static final int IED_KILL_DISTANCE = 5;
    
    private Map<Point, Drone> droneByLocation = new HashMap<Point, Drone>();
    private Map<String, Drone> droneByName = new HashMap<String, Drone>();
    private Map<Point, Ied> iedByLocation = new HashMap<Point, Ied>();
    private GlobalMap map;
    private Point startingPoint = new Point(0, 0);
	private Set<Point> landingLocations;
	
	// As provided by the control agent
	private Map<Point, CellType> knownRoad = new HashMap<Point, CellType>();
	
	// Sets for calulating utility
	private Set<Point> currentSensedRoad = new HashSet<Point>();
	private Set<Point> currentClearedRoad = new HashSet<Point>();
	
    public SimulatorModel(GlobalMap map) {
        this.map = map;
        for (Point p : map.getIedLocations()) {
            iedByLocation.put(p, new Ied(p));
        }
        landingLocations = new HashSet<Point>(map.getLandingLocations());
    }

    /**
     * Add a new Drone at the at the next free road.
     * @param name
     * @return
     */
    
    public synchronized Drone addDrone(String name) {
        Point p = getStartingPoint();
        
        Drone drone = new Drone(name, p);
        droneByLocation.put(drone.getLocation(), drone);
        droneByName.put(drone.getName(), drone);
        
        System.out.println("Adding drone " + drone);
        
        setChanged();
        notifyObservers(new DroneChange(null, drone));
        return drone;
    }


    /**
     * Get the starting point for the agent.
     */
    private synchronized Point getStartingPoint() {
    	Point ret = null;
    	for (Point p : landingLocations) {
    		if (isCellFree(p)) {
    			ret = p;
            	landingLocations.remove(ret);
    			break;
    		}
    	}

    	// No landing points left calculate one
    	if (ret == null) {
            while (!isCellFree(startingPoint) || !map.isRoad(startingPoint)
                    && startingPoint.getX() >= 0) {
                startingPoint = map.findNextRoad(startingPoint);
            }
            ret = startingPoint;
    	}
    	
    	return ret;
    }
    

    public synchronized boolean isCellFree(Point location) {
        if (droneByLocation.containsKey(location)) {            
            return false;
        }
        return true;
    }

    /**
     * Return the drone with the specified name.
     * 
     * @param name
     * @return
     */
    public synchronized Drone getDrone(String name) {
        return droneByName.get(name);
    }
    
    /**
     * Return the set of ieds which a drone at the specified location could sense.
     * 
     * @param location
     * @return
     */
    public synchronized Set<Ied> sensedIeds(Point location) {
        Set<Point> senseLocations = generateSenseSet(location, IED_SENSE_DISTANCE);
 
        Set<Ied> ieds = new HashSet<Ied>();
        for (Point p : senseLocations) {
            if (iedByLocation.containsKey(p)) {
                ieds.add(iedByLocation.get(p));
            }
        }
        return ieds;
    }
    
    /**
     * Return the set of road cells that a drone can see from the specified
     * location.
     * @param drone 
     * @param location
     * @return
     */
    public synchronized Set<Point> senseRoad(Drone drone, Point location) {
    	Set<Point> senses = generateSenseSet(location, ROAD_SENSE_DISTANCE);
    	updateDroneUtility(drone, location, senses);
    	return senses;
    }
    
    
    /**
     * Return the set of other drones which a drone at the specified location could sense.
     * 
     * @param location
     * @return
     */
	public synchronized Set<Drone> senseDrones(Point location) {
        Set<Point> senseLocations = generateSenseSet(location, DRONE_SENSE_DISTANCE);
        
        Set<Drone> ieds = new HashSet<Drone>();
        for (Point p : senseLocations) {
            if (droneByLocation.containsKey(p)) {
                ieds.add(droneByLocation.get(p));
            }
        }
        return ieds;
	}

    
    /**
     * Returns the set of points which are part of the road and which
     * are within the specified radius from the location.
     *   
     * @param radius
     * @return
     */
    private Set<Point> generateSenseSet(Point location, int radius) {
//        System.out.println("SimulatorModel.generateSenseSet: " + location + " "+ radius);
        Set<Point> senseLocations = new HashSet<Point>();
        for (int i=-radius; i<=radius; ++i) {
            for (int j=-radius; j<=radius; ++j) {
//                System.out.print(i + ", " + j + "\t");
//                System.out.print((i*i) + ", " + (j*j) + "\t");
//                System.out.print((radius*radius) + "\t");
                if (( (i*i) + (j*j) ) <= (radius*radius)) {
//                    System.out.print("circle\t");                    
                    Point p = new Point(location.getX() + i, location.getY() + j);
                    if (map.isRoad(p)) {                    
//                        System.out.print("road\t");                    
                        senseLocations.add(p);
                    }
                }
//                System.out.println();
            }
        }
        return senseLocations;
    }

    
    /**
     * Move the specified drone in the specified direction.  Ensure the are no
     * other drones in that location.
     * 
     * @param drone
     * @param direction
     * @throws MoveException
     */
    public synchronized void move(Drone drone, Direction direction) throws MoveException {
        Point newLocation = direction.move(drone.getLocation());
        if (!droneByLocation.containsKey(newLocation)) {
            Point oldLocation = drone.getLocation();
            
            droneByLocation.remove(oldLocation);
            drone.setLocation(newLocation);
            droneByLocation.put(drone.getLocation(), drone);

            setChanged();
            notifyObservers(new DroneChange(oldLocation, drone));
            
            if (iedByLocation.containsKey(newLocation)) {
            	explodeIed(iedByLocation.get(newLocation));
            }
        }
        else {
            throw new MoveException(newLocation, drone);
        }
    }

    /**
     * Blow up ied and kill everything in the kill zone.
     * 
     * @param ied
     */
    private void explodeIed(Ied ied) {
    	if (ied.getStatus() == Ied.Status.Active) {
    		ied.explode();
            setChanged();
            notifyObservers(ied);

            Set<Point> killzone = generateSenseSet(ied.getLocation(), IED_KILL_DISTANCE);
    		
    		for (Point p : killzone) {
    			if (droneByLocation.containsKey(p)) {
    				Drone drone = droneByLocation.get(p);
    				drone.setStatus(Drone.Status.Dead);
    	            setChanged();
    	            notifyObservers(new DroneChange(null, drone));
    			}
    			if (iedByLocation.containsKey(p)) {
    				explodeIed(iedByLocation.get(p));
    			}
    		}
    	}
	}

	public synchronized GlobalMap getMap() {
        return map;
    }

    public synchronized void getDrones(Collection<Drone> drones) {
        drones.addAll(droneByName.values());
    }
    
    public static class DroneChange {
        private Point oldLocation;
        private Drone drone;

        DroneChange(Point oldLocation, Drone drone) {
            this.oldLocation = oldLocation;
            this.drone = drone;
        }

        public Drone getDrone() {
            return drone;
        }

        public Point getOldLocation() {
            return oldLocation;
        }
    }

    public synchronized void getIeds(Collection<Ied> ieds) {
    	ieds.addAll(iedByLocation.values());
    }
    
    public synchronized void updateKnownRoad(Map<Point, CellType> update) {
    	for (Point p : update.keySet()) {
    		CellType newType = update.get(p);
    		if (newType == CellType.DRONE) {
    			newType = CellType.VISITED;
    		}
    		CellType oldType = knownRoad.get(p);
    		
    		if (oldType == null) {
    	    	knownRoad.put(p, newType);    			
    		}
    		else  if (oldType == CellType.ROAD && newType != CellType.ROAD) {
    			knownRoad.put(p, newType);
    		}
    		else if (newType == CellType.IED) {
    			knownRoad.put(p, newType);
    		}
    	}
    	
        setChanged();
        notifyObservers(update.keySet());
    }
    
    public synchronized void getKnownRoad(Map<Point, CellType> road) {
    	road.putAll(knownRoad);
    }

	private void updateDroneUtility(Drone drone, Point location, Set<Point> senses) {
		Set<Point> newCleared = generateSenseSet(location, IED_SENSE_DISTANCE);
		newCleared.removeAll(currentClearedRoad);
		
		// First pass can clear 5 afterwards can only clear 3 cells
		int maxCleared = currentClearedRoad.isEmpty() ? 5 : 3;
		maxCleared = Math.max(maxCleared, newCleared.size());
		
		drone.updateUtility(maxCleared, newCleared.size());

		currentSensedRoad.addAll(senses);
		currentClearedRoad.addAll(newCleared);
	}

	public synchronized int totalSensed() {
		return currentSensedRoad.size();
	}

	public synchronized int totalCleared() {
		return currentClearedRoad.size();
	}
 }
